表单组件:基于 Schema 的嵌套响应式布局
概述
在 Schema 驱动的动态表单中实现栅格布局,支持多个表单项在同一行显示。通过 el-row / el-col 组件实现嵌套响应式布局,当 Schema 中配置了 span 属性时自动渲染为栅格布局,未配置则默认独占一行。
栅格布局原理
Element Plus 栅格系统
Element Plus 基于 24 分栏的栅格系统,通过 span 属性控制每列的宽度占比。
| span 值 | 宽度占比 | 每行可放数量 |
|---|---|---|
| 24 | 100% | 1 个表单项 |
| 12 | 50% | 2 个表单项 |
| 8 | 33.3% | 3 个表单项 |
| 6 | 25% | 4 个表单项 |
条件渲染逻辑
Schema 配置了 span 属性?
→ 是:渲染 el-row > el-col > form-item
→ 否:直接渲染 form-item(独占一行)
text
类型定义
Col 组件 Props
// types/form.ts
import type { ColProps } from 'element-plus'
/** Schema 中的栅格配置 */
export interface FormItemSchema {
field: string
label: string
type: 'input' | 'select' | 'date-picker' | 'time-picker' | 'switch' | 'textarea'
componentProps?: Record<string, unknown>
rules?: unknown[]
hidden?: boolean
/** 栅格占位(1-24),不设置则独占一行 */
span?: number
/** Col 组件的其他 props */
colProps?: Partial<ColProps>
}
typescript
组件实现
Col 包装组件
<!-- components/form/Col.vue -->
<script setup lang="ts">
import type { ColProps } from 'element-plus'
const props = defineProps<{
/** el-col 的配置项,非必填 */
colProps?: Partial<ColProps>
}>()
</script>
<template>
<!-- 传了 colProps(含 span)时使用栅格布局 -->
<el-col v-if="props.colProps" v-bind="props.colProps">
<slot />
</el-col>
<!-- 未传时直接渲染内容,独占一行 -->
<template v-else>
<slot />
</template>
</template>
vue
FormItem 组件集成栅格
<!-- components/form/FormItem.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import Col from './Col.vue'
import type { FormItemSchema } from '../../types/form'
const props = defineProps<{
schema: FormItemSchema
model: Record<string, unknown>
}>()
/** 组件类型映射 */
const componentMap: Record<string, string> = {
'input': 'el-input',
'select': 'el-select',
'date-picker': 'el-date-picker',
'time-picker': 'el-time-picker',
'switch': 'el-switch',
'textarea': 'el-input'
}
/** 栅格配置 */
const colConfig = computed(() => {
if (!props.schema.span) return undefined
return {
span: props.schema.span,
...props.schema.colProps
}
})
/** 是否为 textarea 类型 */
const isTextarea = computed(() => props.schema.type === 'textarea')
</script>
<template>
<Col :col-props="colConfig">
<el-form-item :label="schema.label" :prop="schema.field" :rules="schema.rules">
<component
:is="componentMap[schema.type]"
v-model="model[schema.field]"
:type="isTextarea ? 'textarea' : undefined"
v-bind="schema.componentProps"
class="w-full"
>
<!-- select 的 options -->
<template v-if="schema.type === 'select'">
<el-option
v-for="opt in (schema.componentProps?.options as any[])"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</template>
</component>
</el-form-item>
</Col>
</template>
vue
Schema 驱动的表单组件
<!-- components/form/DynamicForm.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance } from 'element-plus'
import FormItem from './FormItem.vue'
import type { FormSchema } from '../../types/form'
const props = defineProps<{
schema: FormSchema
model: Record<string, unknown>
}>()
const emit = defineEmits<{
submit: [values: Record<string, unknown>]
}>()
const formRef = ref<FormInstance>()
const visibleItems = computed(() =>
props.schema.items.filter(item => !item.hidden)
)
/** 按行分组:连续有 span 的项归为一组 */
const rowGroups = computed(() => {
const groups: FormItemSchema[][] = []
let currentGroup: FormItemSchema[] = []
visibleItems.value.forEach(item => {
if (item.span) {
currentGroup.push(item)
} else {
if (currentGroup.length) {
groups.push(currentGroup)
currentGroup = []
}
groups.push([item])
}
})
if (currentGroup.length) groups.push(currentGroup)
return groups
})
async function handleSubmit(): Promise<void> {
const valid = await formRef.value?.validate().catch(() => false)
if (valid) emit('submit', { ...props.model })
}
</script>
<template>
<el-form ref="formRef" :model="model" v-bind="schema.formProps">
<template v-for="(group, index) in rowGroups" :key="index">
<!-- 有 span 的项放在 el-row 中 -->
<el-row v-if="group[0]?.span" :gutter="20">
<FormItem
v-for="item in group"
:key="item.field"
:schema="item"
:model="model"
/>
</el-row>
<!-- 无 span 的项独占一行 -->
<template v-else>
<FormItem
v-for="item in group"
:key="item.field"
:schema="item"
:model="model"
/>
</template>
</template>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</el-form-item>
</el-form>
</template>
vue
使用示例
三栏布局的 Schema 配置
const schema: FormSchema = {
formProps: {
labelWidth: '80px',
labelPosition: 'right'
},
items: [
// 独占一行
{ field: 'title', label: '标题', type: 'input' },
// 同一行三栏布局
{
field: 'date',
label: '日期',
type: 'date-picker',
span: 8,
componentProps: { placeholder: '选择日期' }
},
{
field: 'time',
label: '时间',
type: 'time-picker',
span: 8,
componentProps: { placeholder: '选择时间' }
},
{
field: 'status',
label: '状态',
type: 'select',
span: 8,
componentProps: {
options: [
{ label: '启用', value: 'active' },
{ label: '禁用', value: 'inactive' }
]
}
},
// 独占一行
{ field: 'description', label: '描述', type: 'textarea' }
]
}
typescript
渲染效果
┌──────────────────────────────────────────┐
│ 标题: [__________________________________] │
├────────────┬────────────┬────────────────┤
│ 日期: [___] │ 时间: [___] │ 状态: [______] │
├────────────┴────────────┴────────────────┤
│ 描述: [__________________________________] │
└──────────────────────────────────────────┘
text
实践要点
- Col 组件通过
v-if条件判断是否渲染el-col,未设置span时直接渲染 slot 内容 colProps设置为Partial<ColProps>类型,所有属性均为可选- 表单组件添加
class="w-full"确保输入框撑满栅格列宽 - 按行分组逻辑:连续有
span的项放入同一个el-row,无span的项独占一行 el-row的gutter属性控制列间距(推荐 20),避免输入框紧贴
↑